Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 157 lines 4.3 kB view raw
1import {AtpAgent} from '@atproto/api' 2 3import {type AnyProfileView} from '#/types/bsky/profile' 4 5type PResp = Awaited<ReturnType<AtpAgent['getProfile']>> 6 7// based on https://github.com/Janpot/escape-html-template-tag/blob/master/src/index.ts 8 9const ENTITIES: { 10 [key: string]: string 11} = { 12 '&': '&amp;', 13 '<': '&lt;', 14 '>': '&gt;', 15 '"': '&quot;', 16 "'": '&#39;', 17 '/': '&#x2F;', 18 '`': '&#x60;', 19 '=': '&#x3D;', 20} 21 22const ENT_REGEX = new RegExp(Object.keys(ENTITIES).join('|'), 'g') 23 24function escapehtml(unsafe: Sub): string { 25 if (Array.isArray(unsafe)) { 26 return unsafe.map(escapehtml).join('') 27 } 28 if (unsafe instanceof HtmlSafeString) { 29 return unsafe.toString() 30 } 31 return String(unsafe).replace(ENT_REGEX, char => ENTITIES[char]) 32} 33 34type Sub = HtmlSafeString | string | (HtmlSafeString | string)[] 35 36export class HtmlSafeString { 37 private _parts: readonly string[] 38 private _subs: readonly Sub[] 39 constructor(parts: readonly string[], subs: readonly Sub[]) { 40 this._parts = parts 41 this._subs = subs 42 } 43 44 toString(): string { 45 let result = this._parts[0] 46 for (let i = 1; i < this._parts.length; i++) { 47 result += escapehtml(this._subs[i - 1]) + this._parts[i] 48 } 49 return result 50 } 51} 52 53export function html(parts: TemplateStringsArray, ...subs: Sub[]) { 54 return new HtmlSafeString(parts, subs) 55} 56 57export const renderHandleString = (profile: AnyProfileView) => 58 profile.displayName 59 ? `${profile.displayName} (@${profile.handle})` 60 : `@${profile.handle}` 61 62class HeadHandler { 63 profile: PResp 64 url: string 65 constructor(profile: PResp, url: string) { 66 this.profile = profile 67 this.url = url 68 } 69 async element(element) { 70 const view = this.profile.data 71 72 const description = view.description 73 ? html` 74 <meta name="description" content="${view.description}" /> 75 <meta property="og:description" content="${view.description}" /> 76 ` 77 : '' 78 const img = view.banner 79 ? html` 80 <meta property="og:image" content="${view.banner}" /> 81 <meta name="twitter:card" content="summary_large_image" /> 82 ` 83 : view.avatar 84 ? html`<meta name="twitter:card" content="summary" />` 85 : '' 86 element.append( 87 html` 88 <meta property="og:site_name" content="Witchsky" /> 89 <meta property="og:type" content="profile" /> 90 <meta property="profile:username" content="${view.handle}" /> 91 <meta property="og:url" content="${this.url}" /> 92 <meta property="og:title" content="${renderHandleString(view)}" /> 93 ${description} ${img} 94 <meta name="twitter:label1" content="Account DID" /> 95 <meta name="twitter:value1" content="${view.did}" /> 96 <link 97 rel="alternate" 98 href="at://${view.did}/app.bsky.actor.profile/self" /> 99 `, 100 {html: true}, 101 ) 102 } 103} 104 105class TitleHandler { 106 profile: PResp 107 constructor(profile: PResp) { 108 this.profile = profile 109 } 110 async element(element) { 111 element.setInnerContent(renderHandleString(this.profile.data)) 112 } 113} 114 115class NoscriptHandler { 116 profile: PResp 117 constructor(profile: PResp) { 118 this.profile = profile 119 } 120 async element(element) { 121 const view = this.profile.data 122 123 element.append( 124 html` 125 <div id="bsky_profile_summary"> 126 <h3>Profile</h3> 127 <p id="bsky_display_name">${view.displayName ?? ''}</p> 128 <p id="bsky_handle">${view.handle}</p> 129 <p id="bsky_did">${view.did}</p> 130 <p id="bsky_profile_description">${view.description ?? ''}</p> 131 </div> 132 `, 133 {html: true}, 134 ) 135 } 136} 137 138export async function onRequest(context) { 139 const agent = new AtpAgent({service: 'https://public.api.bsky.app/'}) 140 const {request, env} = context 141 const origin = new URL(request.url).origin 142 143 const base = env.ASSETS.fetch(new URL('/', origin)) 144 try { 145 const profile = await agent.getProfile({ 146 actor: context.params.handleOrDID, 147 }) 148 return new HTMLRewriter() 149 .on(`head`, new HeadHandler(profile, request.url)) 150 .on(`title`, new TitleHandler(profile)) 151 .on(`noscript`, new NoscriptHandler(profile)) 152 .transform(await base) 153 } catch (e) { 154 console.error(e) 155 return await base 156 } 157}